(define-datatype expr expr?
  (num-exp (num number?))
  (sym-exp (sym symbol?))
  (lambda-exp (ids (list-of symbol?))
              (body expr?))
  (if-exp (test-exp expr?)
          (true-exp expr?)
          (false-exp expr?))
  (prim-app-exp
   (prim prim-op?)
   (exp1 expr?)
   (exp2 expr?))
  (app-exp (rator expr?)
           (rands (list-of expr?))))

(define-datatype prim-op prim-op?
  (add-prim)
  (subtract-prim)
  (mult-prim)
  (div-prim)
  (mod-prim))

(define parse
  (lambda (x)
    (cond
      ((null? x) (eopl:error 'parse "Cant parse ~s" x))
      ((number? x) (num-exp x))
      ((symbol? x) (sym-exp x))
      ((not (pair? x)) (eopl:error 'parse "Should be a list by now, on ~s" x))
      ((eqv? (car x) 'lambda)
       (lambda-exp (cadr x)
                   (parse (caddr x))))
      ((eqv? (car x) 'if)
       (if-exp (parse (cadr x))
               (parse (caddr x))
               (parse (cadddr x))))
      ((eqv? (car x) 'add) (prim-app-exp (add-prim) (parse (cadr x)) (parse (caddr x))))
      ((eqv? (car x) 'sub) (prim-app-exp (subtract-prim) (parse (cadr x)) (parse (caddr x))))
      ((eqv? (car x) 'mul) (prim-app-exp (mult-prim) (parse (cadr x)) (parse (caddr x))))
      ((eqv? (car x) 'div) (prim-app-exp (div-prim) (parse (cadr x)) (parse (caddr x))))
      ((eqv? (car x) 'mod) (prim-app-exp (mod-prim) (parse (cadr x)) (parse (caddr x))))
      (else
       (app-exp (parse (car x))
                (map parse (cdr x)))))))

(define unparse
  (lambda (x)
    (cases expr x
      (num-exp (num) num)
      (sym-exp (sym) sym)
      (lambda-exp (ids body)
                  (list 'lambda ids (unparse body)))
      (if-exp (test-exp true-exp false-exp)
              (list 'if (unparse test-exp) (unparse true-exp) (unparse false-exp)))
      (prim-app-exp (prim exp1 exp2)
                    (list (unparse-prim prim) (unparse exp1) (unparse exp2)))
      (app-exp (rator rands)
               (cons (unparse rator) (map unparse rands))))))

(define unparse-prim
  (lambda (x)
    (cases prim-op x
      (add-prim () 'add)
      (subtract-prim () 'sub)
      (mult-prim () 'mul)
      (div-prim () 'div)
      (mod-prim () 'mod))))

(define evaluate
  (lambda (ast)
    (if (null? ast) (eopl:error 'evaluate "Needs an expression. ~S was sent." ast)
        (cases expr ast
          (num-exp (num) num)
          (sym-exp (sym) sym)
          (if-exp (test-exp true-exp false-exp)
                  (if (eqv? (evaluate test-exp) 0)
                      (evaluate true-exp) (evaluate false-exp)))
          (lambda-exp (ids body) 
                      (begin
                        (display "Note: evaluate is unable to bind variables.")
                        (newline)
                        (evaluate body)))
          (prim-app-exp (prim exp1 exp2) (apply-primitive prim exp1 exp2))
          (app-exp (rator rands)
                   (eopl:error 'evaluate "Unable to evaluate an application expression."))))))


(define apply-primitive 
  (lambda (prim exp1 exp2)
    (cases prim-op prim
      (add-prim () (+ (evaluate exp1) (evaluate exp2)))
      (subtract-prim () (- (evaluate exp1) (evaluate exp2)))
      (mult-prim () (* (evaluate exp1) (evaluate exp2)))
      (div-prim () (/ (evaluate exp1) (evaluate exp2)))
      (mod-prim () (modulo (evaluate exp1) (evaluate exp2)))
      )))
  